﻿using System;
using System.Linq;
using System.Collections.Generic;
using System.Runtime.Serialization;

namespace ExFlash.Tags
{
    public class TagAssistant : IDictionaryIdQueryable
    {        
        private readonly int _swfVersion;
        private readonly IDictionary<ushort, Tag> _idDictionary;
        private readonly BinaryBitReader _reader;
        private AudioCodingFormat _lastDefinedFormat;
        
        public TagAssistant(BinaryBitReader reader, int version, IDictionary<ushort,Tag> idDictionary)
        {
            _reader = reader;
            _swfVersion = version;
            _idDictionary = idDictionary;
        }

        public List<Tag> GetTags()
        {
            List<Tag> tags = new List<Tag>();
            PopulateTags(tags);
            return tags;
        }

        public void PopulateTags(IList<Tag> tags)
        {
            Tag tag = GetTag();

            while (!(tag is EndTag))
            {
                tags.Add(tag);
                tag = GetTag();
            }

            // Add the EndTag.
            tags.Add(tag);
        }
        
        private Tag GetTag()
        {
            Int32 tagOffset = (Int32)_reader.Position;

            UInt16 temp = _reader.ReadUInt16();

            UInt16 code = (UInt16) (temp >> 6);
            int len = temp & 0x3F;

            Boolean isLongHeader = (len == 0x3F);

            if (isLongHeader)
            {
                len = _reader.ReadInt32();
            }

            Tag tag = GetTagByCode(tagOffset, len, code);

            try
            {
                tag.LongHeader = isLongHeader;

                tag.SwfDeserialize(_reader);

                if (_reader.Position - tagOffset > tag.TagHeaderLength + tag.TagLength)
                {
                    throw new SerializationException("Data extends over Tag Length. Most likely Parsing error.");
                }
                
                if ((_reader.Position - tagOffset < tag.TagHeaderLength + tag.TagLength) && (tag is DefineFont3Tag))
                {
                    tag.ParsingComment = "Data is smaller than specified Tag Length.";
                    _reader.Position = tagOffset + tag.TagHeaderLength + tag.TagLength;
                }
                else if (_reader.Position - tagOffset < tag.TagHeaderLength + tag.TagLength)
                {
                    throw new SerializationException("Less Data than Tag Length. Most likely Parsing error.");
                }

                SoundStreamHeadTag soundTag = tag as SoundStreamHeadTag;

                if (soundTag != null)
                {
                    _lastDefinedFormat = soundTag.StreamSoundCompression; 
                }

                ExtractCharacterId(tag);

                CheckReferenceValidity(tag);
            }
            catch (Exception ex)
            {
                tag = new UnknownTag(code, len,
                                     tagOffset,
                                     "Failure in Tag Serialization. Original Type: " +
                                     Enum.GetName(typeof (TagCodes), code) + ".",
                                     ex) 
                                     {LongHeader = isLongHeader};

                _reader.Position = tagOffset + tag.TagHeaderLength;
                tag.SwfDeserialize(_reader);
            }

            return tag;
        }

        private void ExtractCharacterId(Tag tag)
        {
            IHasCharacterId hasCharId;

            if ((hasCharId = tag as IHasCharacterId) != null)
            {
                _idDictionary.Add(hasCharId.Id, tag);
            }
        }

        private void CheckReferenceValidity(Tag tag)
        {
            IReferenceCharacterId refIdTag = tag as IReferenceCharacterId;

            if (refIdTag != null)
            {
                Tag referencedtag;

                if (_idDictionary.TryGetValue(refIdTag.ReferencedId, out referencedtag) == false)
                {
                    throw new SerializationException(tag + " is referencing non-existent id.");
                }

                if (
                    (refIdTag.ReferencedAttributes.Intersect(referencedtag.GetType().GetCustomAttributes(true).OfType<Type>()).
                        Any()) == false)
                {
                    throw new SerializationException("ID exists but doesn't belong to a Tag required by referencing tag");
                }
            }
        }

        private Tag GetTagByCode(int tagOffset, int len, ushort code)
        {
            Tag tag;

            #region Tag Creation Switch

            switch ((TagCodes) code)
            {
                case TagCodes.EndTag:
                    {
                        tag = new EndTag(tagOffset);
                        break;
                    }
                case TagCodes.ShowFrame:
                    {
                        tag = new ShowFrameTag(tagOffset);
                        break;
                    }
                case TagCodes.DefineShape:
                    {
                        tag = new DefineShapeTag(len, tagOffset);
                        break;
                    }
                case TagCodes.PlaceObject:
                    {
                        tag = new PlaceObjectTag(len, tagOffset);
                        break;
                    }
                case TagCodes.RemoveObject:
                    {
                        tag = new RemoveObjectTag(len, tagOffset);
                        break;
                    }
                case TagCodes.DefineBits:
                    {
                        tag = new DefineBitsTag(len, tagOffset);
                        break;
                    }
                case TagCodes.DefineButton:
                    {
                        tag = new DefineButtonTag(len, _swfVersion, tagOffset);
                        break;
                    }
                case TagCodes.JPEGTables:
                    {
                        tag = new JPEGTablesTag(len, tagOffset);
                        break;
                    }
                case TagCodes.SetBackgroundColor:
                    {
                        tag = new SetBackgroundColorTag(tagOffset);
                        break;
                    }
                case TagCodes.DefineFont:
                    {
                        tag = new DefineFontTag(len, tagOffset);
                        break;
                    }
                case TagCodes.DefineText:
                    {
                        tag = new DefineTextTag(len, tagOffset);
                        break;
                    }
                case TagCodes.DoAction:
                    {
                        tag = new DoActionTag(len, _swfVersion, tagOffset);
                        break;
                    }
                case TagCodes.DefineFontInfo:
                    {
                        tag = new DefineFontInfoTag(len, tagOffset, this);
                        break;
                    }
                case TagCodes.DefineSound:
                    {
                        tag = new DefineSoundTag(len, tagOffset);
                        break;
                    }
                case TagCodes.StartSound:
                    {
                        tag = new StartSoundTag(len, tagOffset);
                        break;
                    }
                case TagCodes.DefineButtonSound:
                    {
                        tag = new DefineButtonSoundTag(len, tagOffset);
                        break;
                    }
                case TagCodes.SoundStreamHead:
                    {
                        tag = new SoundStreamHeadTag(len, tagOffset);
                        break;
                    }
                case TagCodes.SoundStreamBlock:
                    {
                        tag = new SoundStreamBlockTag(len, tagOffset, _lastDefinedFormat);
                        break;
                    }
                case TagCodes.DefineBitsLossless:
                    {
                        tag = new DefineBitsLosslessTag(len, tagOffset);
                        break;
                    }
                case TagCodes.DefineBitsJPEG2:
                    {
                        tag = new DefineBitsJpeg2Tag(len, tagOffset);
                        break;
                    }
                case TagCodes.DefineShape2:
                    {
                        tag = new DefineShape2Tag(len, tagOffset);
                        break;
                    }
                case TagCodes.DefineButtonCxform:
                    {
                        tag = new DefineButtonCxformTag(len, tagOffset);
                        break;
                    }
                case TagCodes.Protect:
                    {
                        tag = new ProtectTag(len, _swfVersion, tagOffset);
                        break;
                    }
                case TagCodes.PlaceObject2:
                    {
                        tag = new PlaceObject2Tag(len, _swfVersion, tagOffset);
                        break;
                    }
                case TagCodes.RemoveObject2:
                    {
                        tag = new RemoveObject2Tag(len, tagOffset);
                        break;
                    }
                case TagCodes.DefineShape3:
                    {
                        tag = new DefineShape3Tag(len, tagOffset);
                        break;
                    }
                case TagCodes.DefineText2:
                    {
                        tag = new DefineText2Tag(len, tagOffset);
                        break;
                    }
                case TagCodes.DefineButton2:
                    {
                        tag = new DefineButton2Tag(len, _swfVersion, tagOffset);
                        break;
                    }
                case TagCodes.DefineBitsJPEG3:
                    {
                        tag = new DefineBitsJpeg3Tag(len, tagOffset);
                        break;
                    }
                case TagCodes.DefineBitsLossless2:
                    {
                        tag = new DefineBitsLossless2Tag(len, tagOffset);
                        break;
                    }
                case TagCodes.DefineEditText:
                    {
                        tag = new DefineEditTextTag(len, _swfVersion, tagOffset);
                        break;
                    }
                case TagCodes.DefineSprite:
                    {
                        tag = new DefineSpriteTag(len, _swfVersion, tagOffset);
                        break;
                    }
                case TagCodes.FrameLabel:
                    {
                        tag = new FrameLabelTag(len, _swfVersion, tagOffset);
                        break;
                    }
                case TagCodes.SoundStreamHead2:
                    {
                        tag = new SoundStreamHead2Tag(len, tagOffset);
                        break;
                    }
                case TagCodes.DefineMorphShape:
                    {
                        tag = new DefineMorphShapeTag(len, tagOffset);
                        break;
                    }
                case TagCodes.DefineFont2:
                    {
                        tag = new DefineFont2Tag(len, tagOffset);
                        break;
                    }
                case TagCodes.ExportAssets:
                    {
                        tag = new ExportAssetsTag(len, _swfVersion, tagOffset);
                        break;
                    }
                case TagCodes.ImportAssets:
                    {
                        tag = new ImportAssetsTag(len, _swfVersion, tagOffset);
                        break;
                    }
                case TagCodes.EnableDebugger:
                    {
                        tag = new EnableDebuggerTag(len, tagOffset);
                        break;
                    }
                case TagCodes.DoInitAction:
                    {
                        tag = new DoInitActionTag(len, _swfVersion, tagOffset);
                        break;
                    }
                case TagCodes.DefineVideoStream:
                    {
                        tag = new DefineVideoStreamTag(len, tagOffset);
                        break;
                    }
                case TagCodes.VideoFrame:
                    {
                        tag = new VideoFrameTag(len, tagOffset);
                        break;
                    }
                case TagCodes.DefineFontInfo2:
                    {
                        tag = new DefineFontInfo2Tag(len, tagOffset, this);
                        break;
                    }
                case TagCodes.EnableDebugger2:
                    {
                        tag = new EnableDebugger2Tag(len, _swfVersion, tagOffset);
                        break;
                    }
                case TagCodes.ScriptLimits:
                    {
                        tag = new ScriptLimitsTag(tagOffset);
                        break;
                    }
                case TagCodes.SetTabIndex:
                    {
                        tag = new SetTabIndexTag(tagOffset);
                        break;
                    }
                case TagCodes.FileAttributes:
                    {
                        tag = new FileAttributesTag(tagOffset);
                        break;
                    }
                case TagCodes.PlaceObject3:
                    {
                        tag = new PlaceObject3Tag(len, _swfVersion, tagOffset);
                        break;
                    }
                case TagCodes.ImportAssets2:
                    {
                        tag = new ImportAssets2Tag(len, _swfVersion, tagOffset);
                        break;
                    }
                case TagCodes.DefineFontAlignZones:
                    {
                        tag = new DefineFontAlignZonesTag(len, tagOffset, this);
                        break;
                    }
                case TagCodes.CSMTextSettings:
                    {
                        tag = new CsmTextSettingsTag(len, tagOffset);
                        break;
                    }
                case TagCodes.DefineFont3:
                    {
                        tag = new DefineFont3Tag(len, tagOffset);
                        break;
                    }
                case TagCodes.SymbolClass:
                    {
                        tag = new SymbolClassTag(len, _swfVersion, tagOffset);
                        break;
                    }
                case TagCodes.Metadata:
                    {
                        tag = new MetadataTag(len, _swfVersion, tagOffset);
                        break;
                    }
                case TagCodes.DefineScalingGrid:
                    {
                        tag = new DefineScalingGridTag(len, tagOffset);
                        break;
                    }
                case TagCodes.DoABC:
                    {
                        tag = new DoABCTag(len, _swfVersion, tagOffset);
                        break;
                    }
                case TagCodes.DefineShape4:
                    {
                        tag = new DefineShape4Tag(len, tagOffset);
                        break;
                    }
                case TagCodes.DefineMorphShape2:
                    {
                        tag = new DefineMorphShape2Tag(len, tagOffset);
                        break;
                    }
                case TagCodes.DefineSceneAndFrameLabelData:
                    {
                        tag = new DefineSceneAndFrameLabelDataTag(len, _swfVersion, tagOffset);
                        break;
                    }
                case TagCodes.DefineBinaryData:
                    {
                        tag = new DefineBinaryDataTag(len, tagOffset);
                        break;
                    }
                case TagCodes.DefineFontName:
                    {
                        tag = new DefineFontNameTag(len, _swfVersion, tagOffset);
                        break;
                    }
                case TagCodes.StartSound2:
                    {
                        tag = new StartSound2Tag(len, tagOffset);
                        break;
                    }
                case TagCodes.DefineBitsJPEG4:
                    {
                        tag = new DefineBitsJpeg4Tag(len, tagOffset);
                        break;
                    }
                case TagCodes.DefineFont4:
                    {
                        tag = new DefineFont4Tag(len, tagOffset);
                        break;
                    }
                default:
                    {
                        tag = new UnknownTag(code, len, tagOffset) {ParsingComment = "Unknown tag code - " + code};
                        break;
                    }
            }

            #endregion

            return tag;
        }

        #region Implementation of IDictionaryIdQueryable

        public bool TryGetValue(ushort id, out Tag tag)
        {
            return _idDictionary.TryGetValue(id, out tag);
        }

        #endregion
    }
}
